# 实用工具模块 util
30 秒速记
util是一个工具箱,常用的是promisify、inspect、format、deprecate、debuglog、typespromisify只适合标准错误优先回调(err, value) => {},多结果或多次回调的API不适用inspect是给人看的调试输出,不是序列化格式,别拿它的结果去解析debuglog('foo')只有NODE_DEBUG=foo时才输出,适合留在库代码里的调试日志util.isArray这类老判断函数已废弃,改用Array.isArray和util.types
util 是 Node.js 自带的工具箱,没有统一主题,记住几个高频的就够用。 用得最多的是 promisify,把 fs.readFile 这类错误优先回调的 API 转成返回 Promise 的函数;inspect 是 console.log 打对象时背后用的格式化,调深层对象很方便;debuglog 能写只在设置 NODE_DEBUG 环境变量时才输出的日志;deprecate 给要下线的函数加废弃提示。现在 fs/promises、timers/promises 都有现成的 Promise 版本,promisify 主要用在老的第三方回调库上。
理解「实用工具模块 util」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
util 里几个高频 API 的最小用法:
const util = require('util');
const fs = require('fs');
// 1. promisify:错误优先回调 → Promise
const readFile = util.promisify(fs.readFile);
readFile(__filename, 'utf8').then((s) => console.log(s.length));
// 2. callbackify:反过来,Promise → 错误优先回调
const legacy = util.callbackify(async (x) => x * 2);
legacy(21, (err, v) => console.log(v)); // 42
// 3. format:printf 风格拼字符串,console.log 内部就用它
console.log(util.format('%s 有 %d 道题', '前端', 300)); // 前端 有 300 道题
// 4. inspect:调试输出,控制展开深度
const deep = { a: { b: { c: { d: 1 } } } };
console.log(util.inspect(deep)); // { a: { b: { c: [Object] } } }
console.log(util.inspect(deep, { depth: null })); // { a: { b: { c: { d: 1 } } } }
// 5. deprecate:首次调用时打印一次废弃警告
const oldApi = util.deprecate(() => 'ok', 'oldApi() 已废弃,请改用 newApi()', 'DEP_MY_001');
oldApi(); // 打印 DeprecationWarning,返回 'ok'
// 6. isDeepStrictEqual:深比较
console.log(util.isDeepStrictEqual({ a: [1] }, { a: [1] })); // true
debuglog 要配合环境变量看效果:
const debug = util.debuglog('app');
debug('连接数 %d', 3); // 只有 NODE_DEBUG=app node index.js 时才输出
较新的版本里还有 util.parseArgs(Node.js 18.3+)解析命令行参数,适合写小工具脚本,不用装 yargs;以及 util.styleText 给终端输出加颜色。早年的 util.isArray、util.isString 等类型判断函数已经废弃,分别用 Array.isArray、typeof 和 util.types 代替。
面试官追问
追问 1util.promisify 能转所有回调风格的函数吗?
不能。它要求最后一个参数是 (err, value) 形式的回调、而且只调一次。回调有多个结果值、或者会被多次调用(其实是事件),转出来的结果都不对,这种要手写 new Promise 包装。
追问 2promisify(setTimeout) 能用吗?
能,setTimeout 带了 util.promisify.custom 自定义实现,await promisify(setTimeout)(100) 就是等 100ms。不过现在直接 require('timers/promises').setTimeout 更直观。
追问 3console.log 一个嵌套很深的对象,里面显示 [Object],怎么看全?
默认只展开 2 层。用 console.log(util.inspect(obj, { depth: null })) 展开全部,或者 console.dir(obj, { depth: null })。循环引用它会标成 [Circular *1],不会死循环。
追问 4库里想留调试日志,又不想污染使用者的控制台,怎么做?
const debug = util.debuglog('mylib'),平时 debug('...') 不输出,用户需要时 NODE_DEBUG=mylib node app.js 才打印。不用额外装 debug 包。
追问 5判断一个值是不是 Promise 或者 Date,用 instanceof 有什么问题?
跨 vm 上下文或跨 realm 创建的对象,instanceof 会判断失败。util.types.isPromise(v)、util.types.isDate(v) 是按内部类型判断的,更可靠。
# debuglog(section)
30 秒速记
util.debuglog('foo')返回一个日志函数,平时静默,启动时带NODE_DEBUG=foo才打印- 多个分区用逗号:
NODE_DEBUG=first,second;Node 10+支持通配符NODE_DEBUG=foo* - 输出格式
FOO 6347: hello:分区名大写 + 进程pid+ 消息 logger.enabled(Node 14.9+)可以判断开没开,拼大对象前先判断,省掉白算- 只适合临时排障,审计、业务日志别放这里,没开开关就等于没写
util.debuglog 就是一个带开关的 console.log,开关是环境变量 NODE_DEBUG。 代码里写 const log = util.debuglog('foo'),平时跑 node app.js 什么都不打,出问题时用 NODE_DEBUG=foo node app.js 启动,日志就出来了,格式是 FOO 6347: hello,中间那个数字是进程 pid。好处是调试代码可以一直留在仓库里,不用上线前删。Node 自己的核心模块也是这么埋点的,比如 NODE_DEBUG=http,net 能直接看到 http 模块内部的连接日志,排查连接复用问题很好用。
很有用的调试方法。可以通过 util.debuglog(name) 来创建一个调试fn,这个fn的特点是,只有在运行程序时候,声明环境变量NODE_DEBUG=name,才会打印出调试信息。
可以看下面的例子,直接运行 node debuglog.js,没有任何输出。需要NODE_DEBUG=foo,才会有打印信息.
var util = require('util');
var logger = util.debuglog('foo');
logger('hello');
如下所示,注意,6347 是当前进程id。
➜ 2016.12.02-util git:(master) ✗ NODE_DEBUG=foo node debuglog.js
FOO 6347: hello world
此外,还可以一次指定多个name,通过逗号分隔。
var util = require('util');
var firstLogger = util.debuglog('first');
var secondLogger = util.debuglog('second');
var thirdLogger = util.debuglog('third');
firstLogger('hello');
secondLogger('hello');
thirdLogger('hello');
